/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ablanco.zoomy;

import ohos.agp.components.AttrHelper;
import ohos.agp.components.Component;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.multimodalinput.event.TouchEvent;

/**
 * Detects scaling transformation gestures using the supplied {@link TouchEvent}s.
 * The {@link OnScaleGestureListener} callback will notify users when a particular
 * gesture event has occurred.
 * <p>
 * This class should only be used with {@link TouchEvent}s reported via touch.
 * <p>
 * To use this class:
 * <ul>
 *  <li>Create an instance of the {@code ScaleGestureDetector} for your
 *      {@link Component}
 *  <li>In the {@link Component#setTouchEventListener(Component.TouchEventListener)} method ensure you call
 *          {@link Component#setTouchEventListener(Component.TouchEventListener)}. The methods defined in your
 *          callback will be executed when the events occur.
 * </ul>
 */
public class ScaleGestureDetector {


    /**
     * The listener for receiving notifications when gestures occur.
     * If you want to listen for all the different gestures then implement
     * this interface. If you only want to listen for a subset it might
     * be easier to extend .
     * <p>
     * An application will receive events in the following order:
     * <ul>
     *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
     *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
     *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
     * </ul>
     */
    public interface OnScaleGestureListener {

        /**
         * Responds to scaling events for a gesture in progress.
         * Reported by pointer motion.
         *
         * @param detector The detector reporting the event - use this to
         *                 retrieve extended info about event state.
         * @return Whether or not the detector should consider this event
         * as handled. If an event was not handled, the detector
         * will continue to accumulate movement until an event is
         * handled. This can be useful if an application, for example,
         * only wants to update scaling factors if the change is
         * greater than 0.01.
         */
        public boolean onScale(ScaleGestureDetector detector);

        /**
         * Responds to the beginning of a scaling gesture. Reported by
         * new pointers going down.
         *
         * @param detector The detector reporting the event - use this to
         *                 retrieve extended info about event state.
         * @return Whether or not the detector should continue recognizing
         * this gesture. For example, if a gesture is beginning
         * with a focal point outside of a region where it makes
         * sense, onScaleBegin() may return false to ignore the
         * rest of the gesture.
         */
        public boolean onScaleBegin(ScaleGestureDetector detector);

        /**
         * Responds to the end of a scale gesture. Reported by existing
         * pointers going up.
         * <p>
         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
         * and {@link ScaleGestureDetector#getFocusY()} will return focal point
         * of the pointers remaining on the screen.
         *
         * @param detector The detector reporting the event - use this to
         *                 retrieve extended info about event state.
         */
        public void onScaleEnd(ScaleGestureDetector detector);
    }


    private final Context mContext;

    private final OnScaleGestureListener mListener;

    private float mFocusX;
    private float mFocusY;

    private boolean mQuickScaleEnabled;
    private boolean mStylusScaleEnabled = true;

    private float mCurrSpan;
    private float mPrevSpan;
    private float mInitialSpan;
    private boolean mInProgress;

    private int mSpanSlop;

    private int mMinSpan;

    private final EventHandler mHandler;

    private float mAnchoredScaleStartX;
    private float mAnchoredScaleStartY;
    private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;

    private static final long TOUCH_STABILIZE_TIME = 128; // ms
    private static final float SCALE_FACTOR = .5f;
    private static final int ANCHORED_SCALE_MODE_NONE = 0;
    private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
    private static final int ANCHORED_SCALE_MODE_STYLUS = 2;

    private GestureDetector mGestureDetector;

    private boolean mEventBeforeOrAboveStartingGestureEvent;

    /**
     * Creates a ScaleGestureDetector with the supplied listener.
     * You may only use this constructor from a {@link EventRunner} thread.
     *
     * @param context  the application's context
     * @param listener the listener invoked for all the callbacks, this must
     *                 not be null.
     * @throws NullPointerException if {@code listener} is null.
     */
    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
        this(context, listener, null);
    }

    /**
     * Creates a ScaleGestureDetector with the supplied listener.
     *
     * @param context  the application's context
     * @param listener the listener invoked for all the callbacks, this must
     *                 not be null.
     * @param handler  the handler to use for running deferred listener events.
     * @throws NullPointerException if {@code listener} is null.
     * @see EventHandler#EventHandler(EventRunner)
     */
    public ScaleGestureDetector(Context context, OnScaleGestureListener listener, EventHandler handler) {
        mContext = context;
        mListener = listener;

        mSpanSlop = 16;
        mMinSpan = AttrHelper.convertDimensionToPix(context, "21mm", 449);
        mHandler = handler;

        setQuickScaleEnabled(true);

    }

    /**
     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
     * when appropriate.
     *
     * <p>Applications should pass a complete and consistent event stream to this method.
     * A complete and consistent event stream involves all MotionEvents from the initial
     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
     *
     * @param event     The event to process
     * @param component 触碰component
     * @return true if the event was processed and the detector wants to receive the
     * rest of the MotionEvents in this event stream.
     */
    public boolean onTouchEvent(TouchEvent event, Component component) {
        final int action = event.getAction();
        // Forward the event to check for double tap gesture
        if (mQuickScaleEnabled) {
            mGestureDetector.onTouchEvent(event, component);
        }

        final int count = event.getPointerCount();
        final boolean isStylusButtonDown = (event.getSourceDevice() & TouchEvent.STYLUS) != 0;

        final boolean anchoredScaleCancelled = mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
        final boolean streamComplete =
            action == TouchEvent.PRIMARY_POINT_UP || action == TouchEvent.CANCEL || anchoredScaleCancelled;

        if (action == TouchEvent.PRIMARY_POINT_DOWN || streamComplete) {
            if (primaryPointDown(streamComplete)) {
                return true;
            }
        }
        if (isaBooleanCheck() && !streamComplete && isStylusButtonDown) {
            buttonScaleGesture(event, component);
        }
        final boolean configChanged =
            action == TouchEvent.PRIMARY_POINT_DOWN
                || action == TouchEvent.OTHER_POINT_UP
                || action == TouchEvent.OTHER_POINT_DOWN
                || anchoredScaleCancelled;
        final boolean pointerUp = action == TouchEvent.OTHER_POINT_UP;
        final int skipIndex = pointerUp ? event.getIndex() : -1;
        // Determine focal point
        float sumX = 0;
        float sumY = 0;
        final int div = pointerUp ? count - 1 : count;
        final float focusX;
        final float focusY;
        if (inAnchoredScaleMode()) {
            // In anchored scale mode, the focal pt is always where the double tap
            // or button down gesture started
            focusX = mAnchoredScaleStartX;
            focusY = mAnchoredScaleStartY;
            if (Compat.getTouchY(event, 0, component) < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } else {
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) {
                    continue;
                }
                sumX += Compat.getTouchX(event, i, component);
                sumY += Compat.getTouchY(event, i, component);
            }

            focusX = sumX / div;
            focusY = sumY / div;
        }
        determineAverage(event, component, action, count, configChanged, skipIndex, div, focusX, focusY);
        return true;
    }

    private void buttonScaleGesture(TouchEvent event, Component component) {
        // Start of a button scale gesture
        mAnchoredScaleStartX = Compat.getTouchX(event, 0, component);

        mAnchoredScaleStartY = Compat.getTouchY(event, 0, component);

        mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
        mInitialSpan = 0;
    }

    private void determineAverage(TouchEvent event, Component component, int action, int count, boolean configChanged, int skipIndex, int div, float focusX, float focusY) {
        // Determine average deviation from focal point
        float devSumX = 0;
        float devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) {
                continue;
            }

            // Convert the resulting diameter into a radius.
            devSumX += Math.abs(Compat.getTouchX(event, i, component) - focusX);
            devSumY += Math.abs(Compat.getTouchY(event, i, component) - focusY);
        }
        final float devX = devSumX / div;
        final float devY = devSumY / div;

        // Span is the average distance between touch points through the focal point;
        // i.e. the diameter of the circle with a radius of the average deviation from
        // the focal point.
        final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }

        // Dispatch begin/end events as needed.
        // If the configuration changes, notify the app to reset its current state by beginning
        // a fresh scale event stream.
        final boolean wasInProgress = mInProgress;
        mFocusX = focusX;
        mFocusY = focusY;
        if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
            mListener.onScaleEnd(this);
            mInProgress = false;
            mInitialSpan = span;
        }
        if (configChanged) {
            mInitialSpan = mPrevSpan = mCurrSpan = span;
        }

        final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
        if (!mInProgress && span >= minSpan && (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
            mPrevSpan = mCurrSpan = span;
            mInProgress = mListener.onScaleBegin(this);
        }

        // Handle motion; focal point and span/scale factor are changing.
        if (action == TouchEvent.POINT_MOVE) {
            pointMove(span);
        }
    }

    private void pointMove(float span) {
        mCurrSpan = span;

        boolean updatePrev = true;

        if (mInProgress) {
            updatePrev = mListener.onScale(this);
        }

        if (updatePrev) {
            mPrevSpan = mCurrSpan;
        }
    }

    private boolean primaryPointDown(boolean streamComplete) {
        // Reset any scale in progress with the listener.
        // If it's an ACTION_DOWN we're beginning a new event stream.
        // This means the app probably didn't give us all the events. Shame on it.
        if (mInProgress) {
            mListener.onScaleEnd(this);
            mInProgress = false;
            mInitialSpan = 0;
            mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
        } else if (inAnchoredScaleMode() && streamComplete) {
            mInProgress = false;
            mInitialSpan = 0;
            mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
        }

        if (streamComplete) {
            return true;
        }
        return false;
    }

    private boolean isaBooleanCheck() {
        return !mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode();
    }

    private boolean inAnchoredScaleMode() {
        return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
    }

    /**
     * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
     * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
     * if the app targets API 19 and newer.
     *
     * @param scales true to enable quick scaling, false to disable
     */
    public void setQuickScaleEnabled(boolean scales) {
        mQuickScaleEnabled = scales;
        if (mQuickScaleEnabled && mGestureDetector == null) {
            GestureDetector.SimpleOnGestureListener gestureListener =
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    public boolean onDoubleTap(TouchEvent touchEvent) {
                        // Double tap: start watching for a swipe
                        mAnchoredScaleStartX = touchEvent.getPointerPosition(0).getX();
                        mAnchoredScaleStartY = touchEvent.getPointerPosition(0).getY();
                        mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
                        return true;
                    }
                };
            mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
        }
    }

    /**
     * isInProgress
     *
     * @return boolean
     */
    public boolean isInProgress() {
        return mInProgress;
    }

    /**
     * Get the X coordinate of the current gesture's focal point.
     * If a gesture is in progress, the focal point is between
     * each of the pointers forming the gesture.
     * <p>
     * If {@link #isInProgress()} would return false, the result of this
     * function is undefined.
     *
     * @return X coordinate of the focal point in pixels.
     */
    public float getFocusX() {
        return mFocusX;
    }

    /**
     * getFocusY
     *
     * @return float
     */
    public float getFocusY() {
        return mFocusY;
    }


    /**
     * Return the scaling factor from the previous scale event to the current
     * event. This value is defined as
     * ({@link #mCurrSpan} / {@link #mPrevSpan}).
     *
     * @return The current scaling factor.
     */
    public float getScaleFactor() {
        if (inAnchoredScaleMode()) {
            // Drag is moving up; the further away from the gesture
            // start, the smaller the span should be, the closer,
            // the larger the span, and therefore the larger the scale
            final boolean scaleUp =
                (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan))
                    || (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
            if (mPrevSpan <= mSpanSlop) {
                return 1;
            } else {
                return scaleUp ? (1 + spanDiff) : (1 - spanDiff);
            }

        }
        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    }

}
